import re
import sys
from textwrap import dedent

import piexif

class InteractionError(ValueError):
  def __init__(self, arg):
    super().__init__(arg)


op_path = []
loaded_fname = None
loaded_exif = None
lower_input = False
hinted = False
prompt = ''
opref = -1

while True:
  try:
    def _f():

      global op_path, loaded_fname, loaded_exif, lower_input, hinted, prompt, opref

      if opref == len(op_path):
        inp = input(prompt)
        prompt = ''
        if lower_input:
          inp = inp.lower()
          lower_input = False
        if inp.lower() == 'q':
          if len(op_path):
            op_path.pop()
          else:
            exit()
        else:
          if inp == '\\q' or inp == '\\Q':
            inp = inp[1]
          op_path.append(inp)

      opref = len(op_path)

      if len(op_path) == 0:
        print('')
        print('Search through exifs:')
        print('a) List all IFDs')
        print('b) Search fields in IFD')
        print('c) Load an image file')
        print('d) Print a field in the loaded file')
        print('e) Show example')
        print('q) Quit or return to previous level')
        if not hinted:
          print("* To enter a single 'Q'/'q' instead of returning back, please enter \"\\q\" / \"\\Q\"")
        hinted = True
        if loaded_fname:
          print('[loaded_exif]: {}'.format(loaded_fname))
        print('')
        lower_input = True
        prompt = '> '

      elif op_path == ['a']:
        for name in dir(piexif):
          if name.endswith('IFD'):
            print('piexif.{}'.format(name))
        input('-- Press enter to continue --')
        op_path.clear()

      elif op_path == ['b']:
        print('Which IFD to search: ', end = '')

      elif len(op_path) == 2 and op_path[0] == 'b':
        op_path[1] = op_path[1] + ('IFD' if not op_path[1].endswith('IFD') else '')
        if not op_path[1] in dir(piexif):
          raise InteractionError('Invalid IFD')
        print('What to search: ', end = '')

      elif len(op_path) == 3 and op_path[0] == 'b':
        res = []
        for name in dir(getattr(piexif, op_path[1])):
          if name.lower().find(op_path[2].lower()) != -1:
            res.append((
              'piexif.{}.{}'.format(op_path[1], name),
              getattr(getattr(piexif, op_path[1]), name)
            ))
        if len(res):
          print('Found following:\n')
          for itm in res:
            print('{:<34} {}'.format(itm[0], itm[1]))
        else:
          print('No result found.')
        input('-- Press enter to continue --')
        op_path.clear()

      elif op_path == ['c']:
        print('Enter file name (empty to clear): ', end = '')

      elif len(op_path) == 2 and op_path[0] == 'c':
        if op_path[1] == '':
          loaded_exif = None
          loaded_fname = None
          print('OK')
        else:
          try:
            loaded_exif = piexif.load(op_path[1])
            loaded_fname = op_path[1]
            print('OK')
          except FileNotFoundError:
            raise InteractionError('File not found')
        input('-- Press enter to continue --')
        op_path.clear()

      elif op_path == ['d']:
        if not loaded_exif:
          raise InteractionError('No file loaded')
        print('Enter field name ("*" to print all): ', end = '')

      elif len(op_path) == 2 and op_path[0] == 'd':
        if op_path[1].startswith('piexif.'):
          op_path[1] = op_path[1][len('piexif.') : ]
        if op_path[1] == '*':
          print('Output to file (leave empty if don\'t): ', end = '')
        elif op_path[1] == 'thumbnail':
          print('Output to file: ', end = '')
        elif op_path[1].startswith('0') or op_path[1].isdecimal():
          fieldno = int(op_path[1], base = {'0b': 2, '0o': 8, '0x': 16}.get(op_path[1][0:2].lower(), 10))
          found = []
          for ifd in dir(piexif):
            if not ifd.endswith('IFD'):
              continue
            for field in dir(getattr(piexif, ifd)):
              if getattr(getattr(piexif, ifd), field) == fieldno:
                found.append((ifd, field))
                break
          if not len(found):
            print("Index not found: '{}'".format(fieldno))
            op_path.pop()
          else:
            nonempty = False
            for itm in found:
              for key in {'ImageIFD': ['0th', '1st']}.get(itm[0], [itm[0][ : -3]]):
                try:
                  trunc = min(41, len(str(loaded_exif[key][fieldno])))
                  nonempty = True
                except KeyError:
                  continue
                print('{:<34} {:<12} {}'.format(
                  'piexif.{}.{}'.format(* itm),
                  ':{}:{}'.format(key, fieldno),
                  str(loaded_exif[key][fieldno])[0 : trunc]
                ))
            if not nonempty:
              print('Empty')
            op_path.pop()
        elif op_path[1].startswith('['):
          try:
            _, key, _, _no = re.search(r'''\[((?:'|")?)([^\1]+?)\1\]\[((?:'|")?)(\d+?)\3\]''', op_path[1]).groups()
            fieldno = int(_no, base = {'0b': 2, '0o': 8, '0x': 16}.get(_no[0:2].lower(), 10))
          except AttributeError:
            raise InteractionError("Invalid key: '{}'".format(op_path[1]))
          ifd = {'0th': 'ImageIFD', '1st': 'ImageIFD'}.get(key, '{}IFD'.format(key))
          try:
            ifd_obj = getattr(piexif, ifd)
          except AttributeError:
            raise InteractionError("Invalid IFD: '{}'".format(key))
          found = None
          for field in dir(ifd_obj):
            try:
              if getattr(ifd_obj, field) == fieldno:
                found = field
                break
            except ValueError:
              raise InteractionError('Invalid index: {}'.format(fieldno))
          if not found:
            print('{} IFD has no index {}'.format(key, fieldno))
          else:
            try:
              trunc = min(41, len(str(loaded_exif[key][fieldno])))
            except KeyError:
              print('Empty')
            else:
              print('{:<34} {:<12} {}'.format(
                'piexif.{}.{}'.format(ifd, found),
                ':{}:{}'.format(key, fieldno),
                str(loaded_exif[key][fieldno])[0 : trunc]
              ))
          op_path.pop()
        else:
          try:
            ifd, field = op_path[1].split('.')
          except ValueError:
            raise InteractionError("Invalid field: '{}'".format(op_path[1]))
          ifd = ifd + 'IFD' if not ifd.endswith('IFD') else ifd
          try:
            ifd_obj = getattr(piexif, ifd)
          except AttributeError:
            raise InteractionError("Invalid IFD: '{}'".format(op_path[1].split('.')[0]))
          try:
            fieldno = getattr(ifd_obj, field)
          except AttributeError:
            raise InteractionError("{} has no field '{}'".format(ifd, field))
          found = None
          for key in {'ImageIFD': ['0th', '1st']}.get(ifd, [ifd[ : -3]]):
            try:
              trunc = min(41, len(str(loaded_exif[key][fieldno])))
              found = key
            except KeyError:
              continue
            print('{:<34} {:<12} {}'.format(
              'piexif.{}.{}'.format(ifd, field),
              ':{}:{}'.format(key, fieldno),
              str(loaded_exif[key][fieldno])[0 : trunc]
            ))
          if not found:
            print('Empty')
          op_path.pop()
      
      elif len(op_path) == 3 and op_path[0 : 2] == ['d', '*']:
        fo = open(op_path[2], 'w') if len(op_path[2]) else sys.stdout
        for key in loaded_exif:
          if key == 'thumbnail' and loaded_exif['thumbnail']:
            print('[thumbnail]\n', file = fo)
            print('<JPEG bytes>\n', file = fo)
          else:
            print('[{}]\n'.format(key), file = fo)
            for idx in loaded_exif[key]:
              ifd = {
                '0th': piexif.ImageIFD,
                '1st': piexif.ImageIFD,
                'Exif': piexif.ExifIFD,
                'GPS': piexif.GPSIFD,
                'Interop': piexif.InteropIFD
              }[key]
              field = '<unknown>'
              for itm in dir(ifd):
                if getattr(ifd, itm) == idx:
                  field = itm
              trunc = min(52, len(str(loaded_exif[key][idx])))
              print('{:<5} {:<20} {}'.format(idx, field, str(loaded_exif[key][idx])[0 : trunc]), file = fo)
            print('', file = fo)
        fo.close()
        print('OK')
        input('-- Press enter to continue --')
        op_path.clear()

      elif len(op_path) == 3 and op_path[0 : 2] == ['d', 'thumbnail']:
        if op_path[2] == '':
          raise InteractionError('File path cannot be empty')
        if loaded_exif['thumbnail']:
          fo = open(op_path[2], 'wb')
          fo.write(loaded_exif['thumbnail'])
          fo.close()
          print('OK')
        else:
          print('Empty')
        op_path = ['d']

      elif op_path == ['e']:
        print(dedent(r'''
          >>> exif_dict = piexif.load('example.jpg')
          >>> for ifd in exif_dict:
          ...   if ifd == 'thumbnail' and exif_dict['thumbnail']:
          ...     with open('thumb_example.jpg', 'wb') as fo:
          ...       fo.write(exif_dict['thumbnail'])
          ...   else:
          ...     print('[{}]\n'.format(ifd))
          ...     for idx in exif_dict[ifd]:
          ...       field = '<unknown>'
          ...       ifd_desc = {
          ...         '0th': piexif.ImageIFD,
          ...         '1st': piexif.ImageIFD,
          ...         'Exif': piexif.ExifIFD,
          ...         'GPS': piexif.GPSIFD,
          ...         'Interop': piexif.InteropIFD
          ...       }[ifd]
          ...       for itm in dir(ifd_desc):
          ...         if getattr(ifd_desc, itm) == idx:
          ...           field = itm
          ...       trunc = min(52, len(str(exif_dict[ifd][idx])))
          ...       print('{:<5} {:<20} {}'.format(idx, field, str(exif_dict[ifd][idx])[0 : trunc]))
          ...     print('')
        '''))
        input('-- Press enter to continue --')
        op_path.clear()

      else:
        raise InteractionError('Invalid operation')

    _f()

  except InteractionError as e:
    print(str(e))
    input('-- Press enter to continue --')
    if len(op_path):
      op_path.pop()
  except Exception as e:
    print('Unexpected: (most recent call last)')
    tb = e.__traceback__
    while tb:
      print(
        '  File "{}", line {}, in {}'.format(
          tb.tb_frame.f_code.co_filename,
          tb.tb_lineno,
          tb.tb_frame.f_code.co_name
      ))
      tb = tb.tb_next
    print('{}: {}'.format(type(e).__name__, str(e)))
    input('-- Press enter to continue --')
    if len(op_path):
      op_path.pop()
